/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package ca.jerg.canvas;

import ca.jerg.config.ErgConfigCanvas;
import ca.jerg.config.IErgConfig;
import java.awt.*;
import java.awt.event.MouseEvent;
import javax.swing.*;
import javax.swing.undo.*;
import javax.swing.event.MouseInputListener;
import java.util.ArrayList;
import java.util.Iterator;

import ca.jerg.data.*;
import java.awt.geom.AffineTransform;
import java.util.NoSuchElementException;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoableEdit;

/**
 *
 * @author dave
 */
public class Canvas extends JComponent implements MouseInputListener, UndoableEditListener
{
	// <editor-fold defaultstate="collapsed" desc="Constructors">
	public Canvas()
	{		
		data = new ErgData();
		undoManager= new UndoManager();
		undoSupport = new UndoableEditSupport();
		
		construct();
	}
	
	public Canvas( UndoManager undoManager, UndoableEditSupport undoSupport )
	{
		data = new ErgData();
		data.insert(new ErgPoint(0.0,0));
		data.setEdited(false);
		
		this.undoManager = undoManager;
		this.undoSupport = undoSupport;
		
		construct();
	}
	
	public Canvas( ErgData data, UndoManager undoManager )
	{		
		this.data = data;
		this.undoManager = undoManager;
		construct();
	}
	
	private void construct()
	{
		min = new ErgPoint(0.0,0);
		origin = new ErgPoint(min.getTime(), min.getWatts());
		
		maxXY = new Point();
		minXY = new Point(AXIS_X_OFFSET,AXIS_Y_OFFSET);
	
		ErgConfigCanvas defaults = new ErgConfigCanvas();
		
		max = new ErgPoint(60.0,400);
		
		showGrids = defaults.getBoolean(IErgConfig.KEY_SHOW_GRIDS, ErgConfigCanvas.DEF_SHOW_GRIDS);
		yAxisTickSpace = defaults.getDouble(IErgConfig.KEY_Y_AXIS_TICK_SPACE, ErgConfigCanvas.DEF_Y_AXIS_TICK_SPACE);
		xAxisTickSpace = defaults.getDouble(IErgConfig.KEY_X_AXIS_TICK_SPACE, ErgConfigCanvas.DEF_X_AXIS_TICK_SPACE);
		dataLineColor = new Color(
				defaults.getInt(IErgConfig.KEY_DATA_LINE_COLOUR, ErgConfigCanvas.DEF_DATA_LINE_COLOUR.getRGB()));
		dataLineWidth = (float)defaults.getDouble(IErgConfig.KEY_DATA_LINE_WIDTH, ErgConfigCanvas.DEF_DATA_LINE_WIDTH);
		dataPointColor = new Color(
				defaults.getInt(IErgConfig.KEY_DATA_POINT_COLOUR, ErgConfigCanvas.DEF_DATA_POINT_COLOUR.getRGB()));
		dataPointRadius = defaults.getInt(IErgConfig.KEY_DATA_POINT_RADIUS, ErgConfigCanvas.DEF_DATA_POINT_RADIUS);
		
		// the mouse event listener
		addMouseListener(this);		
		
		// initilize the undo/redo system		
		undoSupport.addUndoableEditListener(this);
	}
	// </editor-fold>
	
	// <editor-fold defaultstate="collapsed" desc="Rendering">
	private void paintAxis( Graphics2D g2 )
	{
		int i;		
		int pos;
		Stroke s = g2.getStroke();
		
		g2.setStroke(new BasicStroke(AXIS_LINE_WIDTH));
		
		g2.drawLine(0, 0, maxXY.x, 0);		
		for ( i=0,pos=0; i<=numMinTicks; i++,pos+=dotsPerMinTick )
		{
			g2.drawLine(pos, 0, pos, TICK_SIZE);
			g2.drawString(String.valueOf(xAxisTickSpace * i + origin.getTime()), pos - 10, TICK_SIZE + 15);
		}
				
		g2.drawLine(0, 0, 0, -maxXY.y);		
		for ( i=0,pos=0; i<=numWattTicks; i++,pos-=dotsPerWattTick )
		{
			g2.drawLine(-TICK_SIZE, pos, 0, pos);
			g2.drawString(String.valueOf(yAxisTickSpace * i + origin.getWatts()), TICK_SIZE - 50,pos + 5);
		}
		
		// the axis labals
		g2.drawString("Time (min)",((maxXY.x - minXY.y) / 2) - 50,TICK_SIZE + 30);

		AffineTransform at = g2.getTransform();
		g2.rotate(-Math.PI / 2);
		g2.drawString("Power (W)",((maxXY.y - minXY.y) / 2) + 10,TICK_SIZE - 60);
		g2.setTransform(at);
		g2.setStroke(s);
	}
	
	private void paintGrids( Graphics2D g2 )
	{
		int i;
		int pos;
		Stroke s = g2.getStroke();
		
		g2.setStroke(new BasicStroke(GRID_LINE_WIDTH));

		for ( i=0,pos=-dotsPerWattTick; i<numWattTicks; i++,pos-=dotsPerWattTick )
			g2.drawLine(0, pos, maxXY.x, pos);			
		
		for ( i=0,pos=dotsPerMinTick; i<numMinTicks; i++,pos+=dotsPerMinTick )
			g2.drawLine(pos, 0, pos, -maxXY.y);
		
		g2.setStroke(s);
	}
	
	private void paintData( Graphics2D g2 )
	{
		Stroke s = g2.getStroke();
		Color c = g2.getColor();
		
		g2.setStroke(new BasicStroke(dataLineWidth));		
		
		ErgPoint epStart = null;
		ErgPoint epEnd = null;
		ErgPoint start = new ErgPoint();
		ErgPoint end = new ErgPoint();
		Point startXY = new Point();
		Point endXY = new Point();
		
        ArrayList<ErgPoint> dataSet = data.getData();
		Iterator i = dataSet.iterator();
			
		try
		{
			epStart = (ErgPoint)i.next();
		}
		catch ( Exception e )
		{
			return;
		}
		
        do
		{
			try
			{	
				epEnd = (ErgPoint)i.next();
			}
			catch ( NoSuchElementException e )
			{
				epEnd = null;
			}
			
			if ( epStart != null )
			{	
				// map from absolute space into graph space
				start.setTime(epStart.getTime() - origin.getTime());				
				start.setWatts(epStart.getWatts() - origin.getWatts());
				
				// map from graph space into display space
				startXY.x = (int)(start.getTime() * (1 / minsPerDot));
				startXY.y = -(int)(start.getWatts() * (1 / wattsPerDot));
			}
	
			if ( epEnd != null )
			{
				// map from absolute space into graph space
				end.setTime(epEnd.getTime() - origin.getTime());
				end.setWatts(epEnd.getWatts() - origin.getWatts());

				// map from graph space into display space
				endXY.x = (int)(end.getTime() * (1 / minsPerDot));
				endXY.y = -(int)(end.getWatts() * (1 / wattsPerDot));
			}

			if ( (epStart != null) && (epEnd != null) )
			{
				g2.setColor(dataLineColor);
				g2.drawLine(startXY.x,startXY.y,endXY.x,endXY.y);
			}
			
			g2.setColor(dataPointColor);
			
			if ( epStart != null )
			{
				g2.fillOval(startXY.x - dataPointRadius, startXY.y - dataPointRadius, 
					dataPointRadius * 2, dataPointRadius * 2);
			}
			
			if ( epEnd != null )
			{
				g2.fillOval(endXY.x - dataPointRadius, endXY.y - dataPointRadius, 
					dataPointRadius * 2, dataPointRadius * 2);
			}
			
			epStart = epEnd;
		} while ( i.hasNext() );

		g2.setStroke(s);
		g2.setColor(c);		
	}
	// </editor-fold>
	
	// <editor-fold defaultstate="collapsed" desc="Overrides">
	@Override
	public void paint( Graphics g )
	{
		Graphics2D g2 = (Graphics2D)g;
				
		// enable anti-aliasing for lines
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);

		// move the origin to the bottom left corner
		g2.translate(minXY.x,getHeight() - minXY.y);

		// determine the maximum lengths for each axis
		maxXY.x = getWidth() - 2 * minXY.x;		
		maxXY.y = getHeight() - 2 * minXY.y;
		
		// calculate grid spacing
		numWattTicks = (int)((max.getWatts() - origin.getWatts()) / yAxisTickSpace);
		if ( numWattTicks > 0 )
			dotsPerWattTick = (int)((maxXY.y - minXY.y) / numWattTicks);
		
		numMinTicks = (int)((max.getTime() - origin.getTime()) / xAxisTickSpace);
		if ( numMinTicks > 0 )
			dotsPerMinTick = (int)((maxXY.x - minXY.x) / numMinTicks);
		
		minsPerDot = (max.getTime() - origin.getTime()) / (maxXY.x - minXY.x);
		wattsPerDot = ((double)(max.getWatts() - origin.getWatts())) / (maxXY.y - minXY.y);
		
		// render
		paintAxis(g2);
		
		if ( showGrids == true )
			paintGrids(g2);
		
		/*
		 * setup the clip region to be inside the axis
		 * 
		 * the chip rectangle is defines by a start point
		 * and then a height and width.  The height and width
		 * must be positive so we start at the top left of screen
		 * space (0,-maxXY.y) and have a width of maxXY.x and a
		 * height of maxXY.y
		 */
		g2.setClip(0,-maxXY.y,maxXY.x,maxXY.y);
		paintData(g2);
	}

	@Override
	public Dimension getPreferredSize()
	{
        return new Dimension(200, 200);
    }
	//</editor-fold>
	
	// <editor-fold defaultstate="collapsed" desc="Cursor Location Events">
	public synchronized void addCursorLocationListener( ListenerCursorLocation l )
	{
		listeners.add(l);
	}
	
	public synchronized void removeCursorLocationListener( ListenerCursorLocation l )
	{
		listeners.remove(l);
	}
	
	public synchronized void fireCursorLocationEvent( ErgPoint location )
	{
		EventCursorLocation event = 
				new EventCursorLocation(this, location);
		
		Iterator i = listeners.iterator();
		while ( i.hasNext() )
		{
			((ListenerCursorLocation)i.next()).cursorLocation(event);
		}
	}
	// </editor-fold>
	
	// <editor-fold defaultstate="collapsed" desc="Mouse Input Listener">
	public void mouseClicked( MouseEvent e )
	{	
		// <editor-fold defaultstate="collapsed" desc="Undo/Redo">
		/*
		 * pressing the right mouse button will
		 * perform an UNDO
		 */
		if ( e.getButton() == MouseEvent.BUTTON3 )
		{
			try
			{
				undoManager.undo();
			}
			catch ( CannotUndoException undo )
			{
			}
			
			return;
		}
		
		/*
		 * pressing the wheel button will perform
		 * a REDO
		 */
		if ( e.getButton() == MouseEvent.BUTTON2 )
		{
			try
			{
				undoManager.redo();
			}
			catch ( CannotRedoException redo )
			{
			}
			
			return;
		}
		// </editor-fold>
		
		Point p = e.getPoint();
		if ( (p.x < minXY.x) || ((p.x - minXY.x) > maxXY.x) ||
				(p.y < minXY.y) || ((p.y - minXY.y) > maxXY.y) )
		{
			/*
			 * these clicks are outside the bounds
			 * of graph space so just ignore them
			 */
			return;
		}
		
		// map into graph space
		p.x -= minXY.x;
		p.y = maxXY.y - (p.y - minXY.y);
		
		
		double x = p.x * minsPerDot + origin.getTime();
		double y = p.y * wattsPerDot + origin.getWatts();
		
		/*
		 * if control is down during the click
		 * then just report the current (time,watt)
		 * under the cursor
		 */
		if ( e.isControlDown() == true )
		{
			fireCursorLocationEvent(new ErgPoint(x,(int)y));
			return;
		}
		
		try
		{
			/*
			 * can only add new points later in time
			 * so make sure this point is later in time
			 * then the last one
			 */
			ErgPoint last = data.getLastPoint();
			if ( last.getTime() > (p.x * minsPerDot + origin.getTime()) )
				return;
						
			/*
			 * if shift is down during the click
			 * then do an orthogonal snap
			 */
			if ( e.isShiftDown() == true )
			{
				double deltaX = Math.abs(x - last.getTime());
				double deltaY = Math.abs(y - last.getWatts());
				
				if ( deltaX > deltaY )
				{					
					y = last.getWatts() - origin.getWatts();
				}
				else if ( deltaY > deltaX )
				{
					x = last.getTime() - origin.getTime();
				}
			}
		}
		catch ( IndexOutOfBoundsException ioobe )
		{
		}
		
		ErgPoint ep = new ErgPoint(x, (int)y);
		
		// record the action
		UndoableEdit edit = new EditAdd(this, data, ep);
		
		// perform the action
		data.insert(ep);

		// notify undo listeners
		undoSupport.postEdit(edit);
		
		repaint();
	}

	// <editor-fold defaultstate="collapsed" desc="Unused Mouse Inputs">
	public void mousePressed( MouseEvent e )
	{		
	}

	public void mouseReleased( MouseEvent e )
	{
	}

	public void mouseEntered( MouseEvent e )
	{
	}

	public void mouseExited( MouseEvent e )
	{
	}

	public void mouseDragged( MouseEvent e )
	{
	}

	public void mouseMoved( MouseEvent e )
	{
	}
	// </editor-fold>
	
	// </editor-fold>
	
	// <editor-fold defaultstate="collapsed" desc="Undo Listener">
	public void undoableEditHappened( UndoableEditEvent event )
	{
		UndoableEdit edit = event.getEdit();
		undoManager.addEdit(edit);
	}
	// </editor-fold>
	
	// <editor-fold defaultstate="collapsed" desc="Constents">
	public static final int AXIS_X_OFFSET = 75;
	public static final int AXIS_Y_OFFSET = 50;
	public static final int TICK_SIZE = 5;
	public static final float GRID_LINE_WIDTH = (float)0.1;
	public static final float AXIS_LINE_WIDTH = (float)1.0;
	//public static final float DATA_LINE_WIDTH = (float)0.75;
	//public static final int DATA_POINT_RADIUS = 2;
	// </editor-fold>
	
	// cursor events
	private ArrayList<ListenerCursorLocation> listeners = 
			new ArrayList<ListenerCursorLocation>();
	
	// undo related
	UndoManager undoManager;
	UndoableEditSupport undoSupport;
	
	// data related properties
	private ErgData data;
	private ErgPoint origin;
	private ErgPoint max;
	private ErgPoint min;
	
	// graphing related properties
	private Point maxXY;
	private Point minXY;

	// display related properties
	private int dotsPerWattTick;
	private int dotsPerMinTick;
	private double minsPerDot;
	private double wattsPerDot;
	
	private boolean showGrids;
	private double yAxisTickSpace;
	private double xAxisTickSpace;
	private int numWattTicks;
	private int numMinTicks;
	private Color dataLineColor;
	private float dataLineWidth;
	private Color dataPointColor;
	private int dataPointRadius;

	// <editor-fold defaultstate="collapsed" desc="Accessors">
	public ErgData getData()
	{
		return data;
	}

	public void setData( ErgData data )
	{
		this.data = data;
	}
	
	public Color getDataLineColor()
	{
		return dataLineColor;
	}

	public void setDataLineColor( Color dataLineColor )
	{
		this.dataLineColor = dataLineColor;
	}

	public float getDataLineWidth()
	{
		return dataLineWidth;
	}

	public void setDataLineWidth( float dataLineWidth )
	{
		this.dataLineWidth = dataLineWidth;
	}

	public Color getDataPointColor()
	{
		return dataPointColor;
	}

	public void setDataPointColor( Color dataPointColor )
	{
		this.dataPointColor = dataPointColor;
	}

	public int getDataPointRadius()
	{
		return dataPointRadius;
	}

	public void setDataPointRadius( int dataPointRadius )
	{
		this.dataPointRadius = dataPointRadius;
	}

	public ErgPoint getMax()
	{
		return max;
	}

	public void setMax( ErgPoint max )
	{
		this.max = max;
	}

	public ErgPoint getMin()
	{
		return min;
	}

	public void setMin( ErgPoint min )
	{
		this.min = min;
		origin.setTime(min.getTime());
		origin.setWatts(min.getWatts());
	}

	public int getNumMinTicks()
	{
		return numMinTicks;
	}

	public void setNumMinTicks( int numMinTicks )
	{
		this.numMinTicks = numMinTicks;
	}

	public int getNumWattTicks()
	{
		return numWattTicks;
	}

	public void setNumWattTicks( int numWattTicks )
	{
		this.numWattTicks = numWattTicks;
	}

	public boolean isShowGrids()
	{
		return showGrids;
	}

	public void setShowGrids( boolean showGrids )
	{
		this.showGrids = showGrids;
	}

	public double getXAxisTickSpace()
	{
		return xAxisTickSpace;
	}

	public void setXAxisTickSpace( double xAxisTickSpace )
	{
		this.xAxisTickSpace = xAxisTickSpace;
	}

	public double getYAxisTickSpace()
	{
		return yAxisTickSpace;
	}

	public void setYAxisTickSpace( double yAxisTickSpace )
	{
		this.yAxisTickSpace = yAxisTickSpace;
	}
	// </editor-fold>
}

